Pro Entity Framework Core 2 for ASP.NET Core MVC 翻译

第 24 章 使用事务

作者:Adam Freeman
翻译:陈广
日期:2019-6-8


本章,我描述了 Entity Framework Core 支持事务的方式,事务用于确保为单个工作单元执行多个操作。如果所有操作的执行都没有问题,那么这些更改将应用到数据库中,这就是所谓的提交(committing)事务。如果一个或多个操作失败,则数据库不做改变,修改被放弃,这被称为事务回滚(rolling back)。(我在这里简化了一点,因为事务可能很复杂,但这是事务工作的本质。)

我从描述默认情况下 Entity Framework Core 应用的行为开始,然后向您展示如何直接控制事务,包括如何禁用它们。表24-1为本章简述。

注意:并非所有的数据库服务器都支持事务。本章中的示例用于 SQL Server,如果使用不同的数据库服务器,您可能会看到不同的行为。

表 24-1:事务功能简述

问题 回答
它们是什么? 事务允许将更改组合在一起,以便在所有更改都成功的情况下应用,如果任何更改失败,则不应用。
它们有何用途? 事务允许将关联操作组合在一起,以确保应用程序使用的数据的一致性,而不是数据库服务器强制执行的关系一致性。
如何使用它们 默认情况下,事务是启用的,但也有一个用于配置如何使用事务的 API。
是否有任何缺陷或限制? 事务会影响性能,并且有可能创建死锁,因为在处理事务时必须排队等待更新。
有没有其他选择? 应用程序可以进行校正更新以模拟回滚事务的效果,但这很难正确完成。

表24-2:本章摘要

问题 解决方案 清单
隔离执行更新 每次更新之后调用SaveChanges方法 5
禁用自动事务 使用AutoTransactionsEnabled属性 6,7
显式使用事务 使用BeginTransactionCommitTransactionRollbackTransaction方法
指定事务隔离等级 使用IsolationLevel枚举

准备本章

在本章中,我继续使用第19章创建,并在之后每章一直使用的 AdvancedApp 项目。在第23章中,我使用原始 SQL 创建存储过程和视图等功能,这些不再需要。在 AdvancedApp 文件夹中运行清单24-1所示的命令,以删除并重新创建数据库。

清单 24-1:重置数据库

dotnet ef database drop --force
dotnet ef database update

为了准备本章,我在 Controllers 文件夹中添加了一个名为 MultiController.cs 的类文件,并使用它来定义清单24-2所示的类。

清单 24-2:Controllers 文件夹下的 MultiController.cs 文件的内容

using AdvancedApp.Models;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;

namespace AdvancedApp.Controllers
{
    public class MultiController : Controller
    {
        private AdvancedContext context;
        private ILogger<MultiController> logger;

        public MultiController(AdvancedContext ctx, ILogger<MultiController> log)
        {
            context = ctx;
            logger = log;
        }

        public IActionResult Index()
        {
            return View("EditAll", context.Employees);
        }

        [HttpPost]
        public IActionResult UpdateAll(Employee[] employees)
        {
            context.UpdateRange(employees);
            context.SaveChanges();
            return RedirectToAction(nameof(Index));
        }
    }
}

为向新控制器提供一个视图,我创建了 Views/Multi 文件夹,并在其中添加了一个名为 EditAll.cshtml 的文件,内容如清单24-3所示。

提示:如果您不想跟随构建示例项目的过程,可以从本书的源代码库下载所有所需的文件,这些文件可在 https://github.com/apress/pro-ef-core-2-for-asp.net-core-mvc 上找到。

清单 24-3:Views/Multi 文件夹下的 EditAll.cshtml 文件的内容

@model IEnumerable<Employee>
@{
    ViewData["Title"] = "Advanced Features";
    Layout = "_Layout";
    int counter = 0;
}

<h4 class="bg-info p-2 text-center text-white">
    Edit All
</h4>
<form asp-action="UpdateAll" method="post">
    <div class="container">
        @foreach (Employee e in Model)
        {
            <div class="form-row">
                <div class="col">
                    <input class="form-control" name="Employees[@counter].SSN"
                           value="@e.SSN" readonly />
                </div>
                <div class="col">
                    <input class="form-control" name="Employees[@counter].FirstName"
                           value="@e.FirstName" readonly />
                </div>
                <div class="col">
                    <input class="form-control" name="Employees[@counter].FamilyName"
                           value="@e.FamilyName" readonly />
                </div>
                <div class="col">
                    <input class="form-control" name="Employees[@counter].Salary"
                           value="@e.Salary" />
                </div>
            </div>
            counter++;
        }
    </div>
    <div class="text-center m-2">
        <button type="submit" class="btn btn-primary">Save All</button>
        <a class="btn btn-secondary" asp-action="Index"
           asp-controller="Home">
            Cancel
        </a>
    </div>
</form>

使用dotnet run启动应用程序,导航至 http://localhost:5000,单击【Create】按钮,并使用表24-3提详细信息填充数据库。

SSN FirstName FamilyName Salary Other Name In Active Use
420-39-1864 Bob Smith 100000 Robert Checked
657-03-5898 Alice Jones 200000 Allie Checked
300-30-0522 Peter Davies 180000 Pette Checked

当您完成数据的添加,将看到图24-1所示的结果。

图24-1 运行示例应用程序

理解默认行为

事务是使用数据库的一个基本部分,因此 Entity Framework Core 会自动使用它们。为了揭示事务是如何使用的,我更改了日志系统的配置,以便 Entity Framework Core 能够报告更详细的消息,如清单24-4所示。

清单 24-4:AdvancedApp 文件夹下的 appsetttings.json 文件,更改日志级别

{
  "ConnectionStrings": {
    "DefaultConnection": "Server=(localdb)\\MSSQLLocalDB;Database=AdvancedDb;MultipleActiveResultSets=true"
  },
  "Logging": {
    "LogLevel": {
      "Default": "None",
      "Microsoft.EntityFrameworkCore": "Information",
      "Microsoft.EntityFrameworkCore.Database.Transaction": "Debug"
    }
  }
}

新条目请求来自处理 Entity Framework Core 事务的类的Debug级别消息。要查看默认情况下如何使用事务,请使用dotnet run启动应用程序,导航到 http://localhost:5000/multi,并更改表24-4中描述的Salary值。

表 24-4:成功事务的 Salary 值

名称 Salary 值
Bob Smith 150000
Alice Jones 250000

单击【Save All】按钮,Entity Framework Core 将更新数据库。如果检查数据库中的日志消息,可以看到事件的顺序(尽管很难在由调试日志设置生成的输出流中找到单个消息)。首先,Entity Framework Core 创建一个新事务。

...
Beginning transaction with isolation level 'ReadCommitted'.
...

稍后我将解释什么隔离(isolation)级别,但对于本节来说,重要的是事务已经启动。

我还没有为 Entity Framework Core 提供更改检测的基线,因此更新了每个对象,从而生成了三个UPDATE语句,每个语句后面都有一个SELECT语句来确定数据库生成的值。

...
UPDATE [Employees] SET [LastUpdated] = @p0, [Salary] = @p1, [SoftDeleted] = @p2
WHERE [SSN] = @p3 AND [FirstName] = @p4 AND [FamilyName] = @p5;
SELECT [GeneratedValue]
FROM [Employees]
WHERE @@ROWCOUNT = 1 AND [SSN] = @p3 AND [FirstName] = @p4 AND [FamilyName] = @p5;

UPDATE [Employees] SET [LastUpdated] = @p6, [Salary] = @p7, [SoftDeleted] = @p8
WHERE [SSN] = @p9 AND [FirstName] = @p10 AND [FamilyName] = @p11;
SELECT [GeneratedValue]
FROM [Employees]
WHERE @@ROWCOUNT = 1 AND [SSN] = @p9 AND [FirstName] = @p10 AND [FamilyName] = @p11;

UPDATE [Employees] SET [LastUpdated] = @p12, [Salary] = @p13, [SoftDeleted] = @p14
WHERE [SSN] = @p15 AND [FirstName] = @p16 AND [FamilyName] = @p17;
SELECT [GeneratedValue]
FROM [Employees]
WHERE @@ROWCOUNT = 1 AND [SSN] = @p15 AND [FirstName] = @p16 AND [FamilyName] = @p17;
...

数据库服务器没有报告这些命令的任何错误,Entity Framework Core 没有任何进一步的工作要执行;因此,事务被提交,它将更改应用于数据库,并使用以下消息报告:

...
Committing transaction.
...

在事务提交之前,这些更改不会应用于数据库,如果回滚事务,则可以丢弃这些更改。若要查看回滚操作,请导航到 http://localhost:5000/multi 并更改表24-5中描述的Salary值。

表 24-5:失败事务的 Salary 值

名称 Salary 值
Bob Smith 900000000
Alice Jones 300000

Bob Smith 的Salary值太大,无法使用数据模型中配置的数据类型表示,单击【Save All】按钮时将看到异常。异常中断更新,事务未提交。由于表24-5中的两个更新都是在同一个事务中执行的,所以两个更新都不会应用到数据库中。

执行独立更改

默认事务行为的一个潜在缺陷是,如果将成功的更新与失败的更新组合在一起,则无法将成功的更新应用到数据库。例如,在上一节中,您看到了 Alice Jones 的有效更新是如何失败的,因为它与 Bob Smith 的失败更新组合在一起。如果希望隔离每个更新,使其不受其他故障的影响,则可以为每个更新调用SaveChanges方法,如清单24-5所示。

清单 24-5:Controllers 文件夹下的 MultiController.cs 文件,执行独立更改

using AdvancedApp.Models;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
using System;
using Microsoft.EntityFrameworkCore;

namespace AdvancedApp.Controllers
{
    public class MultiController : Controller
    {
        private AdvancedContext context;
        private ILogger<MultiController> logger;

        public MultiController(AdvancedContext ctx, ILogger<MultiController> log)
        {
            context = ctx;
            logger = log;
        }

        public IActionResult Index()
        {
            return View("EditAll", context.Employees);
        }

        [HttpPost]
        public IActionResult UpdateAll(Employee[] employees)
        {
            foreach (Employee e in employees)
            {
                try
                {
                    context.Update(e);
                    context.SaveChanges();
                }
                catch (Exception)
                {
                    context.Entry(e).State = EntityState.Detached;
                }
            }
            return RedirectToAction(nameof(Index));
        }
    }
}

每个Employee对象分别被传递给 context 对象的Update方法,然后调用SaveChanges方法。我使用了一个try...catch块,以确保更新引发的异常不会中断后续更新。

其结果是,每个更新都将使用一个单独的事务,您可以通过使用dotnet run启动应用程序、导航到 http://localhost:5000/multi,并应用表24-5中的更改来看到这一点。对 Bob Smith 的更新仍将失败,但这次对 Alice Jones 的更新将应用于数据库,如图24-2所示。

提示:注意,我在try … catch块的catch子句中更改了更新失败的Employee对象的更改跟踪状态为Detached。如果不进行此更改,Entity Framework Core 将在下一次调用SaveChanges方法时包含失败的更新,从而导致另一个错误并阻止其他更新的应用。

图24-2 在自己的事务中应用更新

如果检查应用程序生成的日志消息,您将看到为每个对象创建和提交了一个单独的事务。为 Bob Smith 更新引发的异常会阻止该更改的事务被提交,但不会影响将其他更改应用于数据库。

禁用自动事务

如果不希望 Entity Framework Core 自动使用事务,则可以通过更改 context 对象的配置来禁用此功能。context 对象定义了一个Database属性,它返回DatabaseFacade对象,该对象提供对事务功能的访问。此类定义表24-6中描述的属性,它用于控制自动事务功能。

表 24-6:自动事务的 DatabaseFacade 属性

名称 描述
AutoTransactionsEnabled 将此属性设置为false将禁用自动事务功能。

在清单24-6中,我使用了表24-6中描述的属性来禁用AdvancedContext类的自动事务功能。必须为创建的每个对象设置此属性,这意味着使用构造函数可以确保在使用依赖注入的 ASP.NET Core MVC 应用程序中获得一致的结果。

清单 24-6:Models 文件夹下的 AdvancedContext.cs 文件,禁用自动事务

using Microsoft.EntityFrameworkCore;
using System;

namespace AdvancedApp.Models
{
    public class AdvancedContext : DbContext
    {
        public AdvancedContext(DbContextOptions<AdvancedContext> options)
            : base(options)
        {
            Database.AutoTransactionsEnabled = false;
        }
        public DbSet<Employee> Employees { get; set; }
        protected override void OnModelCreating(ModelBuilder modelBuilder) 
        {
           //...此处省略...
        }
    }
}

在清单24-7中,我将UpdateAll方法返回给它的原始实现,因此所有更新都是通过对SaveChanges方法的单个调用来执行的。这将使所有更新都在单个事务中执行。

清单 24-7:Controllers 文件夹下的 MultiController.cs 文件,执行更新

using AdvancedApp.Models;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
using System;
using Microsoft.EntityFrameworkCore;

namespace AdvancedApp.Controllers
{
    public class MultiController : Controller
    {
        private AdvancedContext context;
        private ILogger<MultiController> logger;

        public MultiController(AdvancedContext ctx, ILogger<MultiController> log)
        {
            context = ctx;
            logger = log;
        }

        public IActionResult Index()
        {
            return View("EditAll", context.Employees);
        }

        [HttpPost]
        public IActionResult UpdateAll(Employee[] employees)
        {
            context.UpdateRange(employees);
            context.SaveChanges();
            return RedirectToAction(nameof(Index));
        }
    }
}

若要查看配置更改如何影响结果,请使用dotnet run启动应用程序并导航到 http://localhost:5000/multi。进行表24-7中显示的更改,然后单击【Save Changes】按钮。

表 24-7:用于测试禁用自动事务处理的 Salary 更改

名称 Salary 值
Peter Davies 200000
Bob Smith 900000000
Alice Jones 200000

单击【Save Al】按钮时,将看到一个异常,因为【Bob Smith】的值对于用于在数据库中存储Salary值的数据类型来说太大。但是,由于没有要回滚的事务,所以将其他更改应用于数据库,如图24-3所示。

提示:在不使用事务的情况下,您可能并不总是看到相同的结果,因为数据库服务器或提供程序包可能以不同的方式响应。需要注意的重要一点是,如果其中一个更新失败,则不能依赖所有正在回滚的更新。

图24-3 没有自动事务的工作

使用显式事务

对于大多数项目来说,默认行为已经足够了,但是您可以让 Entity Framework Core 使用一个事务显式地将单独的操作组合在一起。如果您需要更改事务的配置方式或需要在决定是否提交或回滚一组更新之前执行其他任务,这将非常有用。

在清单248中,我修改了 Multi 控制器的UpdateAll方法,以便它显式使用事务。

清单 24-8:Controllers 文件夹下的 MultiController.cs 文件,使用事务

using AdvancedApp.Models;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
using System;
using Microsoft.EntityFrameworkCore;

namespace AdvancedApp.Controllers
{
    public class MultiController : Controller
    {
        private AdvancedContext context;
        private ILogger<MultiController> logger;

        public MultiController(AdvancedContext ctx, ILogger<MultiController> log)
        {
            context = ctx;
            logger = log;
        }

        public IActionResult Index()
        {
            return View("EditAll", context.Employees);
        }

        [HttpPost]
        public IActionResult UpdateAll(Employee[] employees)
        {
            context.Database.BeginTransaction();
            try
            {
                context.UpdateRange(employees);
                context.SaveChanges();
                context.Database.CommitTransaction();
            }
            catch (Exception)
            {
                context.Database.RollbackTransaction();
            }
            return RedirectToAction(nameof(Index));
        }
    }
}

对事务的直接访问是通过 context 对象的Database属性进行的,该属性返回一个DatabaseFacade对象,该对象定义表24-8中描述的与事务相关的成员。

表 24-8:DatabaseFacade 事务成员

名称 描述
BeginTransaction() 此方法创建一个新的事务。这个方法有一个异步版本,名为BeginTransactionAsync
CommitTransaction() 此方法提交当前事务
RollbackTransaction() 此方法回滚当前事务
CurrentTransaction 此属性返回当前事务

在清单中,我调用了BeginTransaction启动一个新的事务,然后执行更新。在调用SaveChanges方法后,Entity Framework Core 向数据库服务器发送 SQL 命令以执行更新,但与使用自动事务功能时不同,它不会自动提交事务。相反,在调用CommitTransaction方法之前,不会将更改应用到数据库。我使用了一个try…catch块,通过调用RollbackTransaction方法来处理数据库服务器报告的任何错误,该方法将放弃更改。


理解事务处理

您经常会看到使用using子句创建的事务,该子句获取了BeginTransaction方法的结果,如下所示:

...
using (var transaction = context.Database.BeginTransaction()) {
    // ...operations are performed...
    context.Database.CommitTransaction();
}
...

事务在被释放时会自动回滚,使用using子句的思想是确保在事务不再需要时立即进行处理,从而防止事务堆叠起来,等待回滚,并销毁它们的对象。

您不必担心在 ASP.NET Core MVC 应用程序中处理事务,因为对象是在 action 方法完成时被释放的。这意味着,除非显式调用CommitTransactionRollbackTransaction方法,否则将在执行 action 方法后回滚事务。


在事务中包含其他操作

前面的示例展示了如何直接处理事务,但不执行任何使用自动事务无法同样工作的任务。直接使用事务的一个常见原因是,在确定是否应该提交或回滚更新之前,在将更新发送到数据库后执行一些额外的工作。作为演示,我向UpdateAll方法添加了一个验证查询,该方法查询数据库以对Salary属性的累积值设置上限,并在总量太大时回滚事务,如清单24-9所示。

清单 24-9:Controllers 文件夹下的 MultiController.cs 文件,执行额外工作

using AdvancedApp.Models;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
using System;
using Microsoft.EntityFrameworkCore;

namespace AdvancedApp.Controllers
{
    public class MultiController : Controller
    {
        private AdvancedContext context;
        private ILogger<MultiController> logger;

        public MultiController(AdvancedContext ctx, ILogger<MultiController> log)
        {
            context = ctx;
            logger = log;
        }

        public IActionResult Index()
        {
            return View("EditAll", context.Employees);
        }

        [HttpPost]
        public IActionResult UpdateAll(Employee[] employees)
        {
            context.Database.BeginTransaction();
            try
            {
                context.UpdateRange(employees);
                context.SaveChanges();
                context.Database.CommitTransaction();
            }
            catch (Exception)
            {
                context.Database.RollbackTransaction();
            }
            return RedirectToAction(nameof(Index));
        }
    }
}

在调用SaveChanges方法之后,我使用Sum方法来得到Salary值的总和。如果事务总数少于100万,则提交事务,否则将其回滚并抛出异常。要查看效果,请启动应用程序,导航到 http://localhost:5000/multi,并更改Salary值。单击【Save All】按钮,您将看到您的更改是如何应用的,或者显示了一个异常,更改被丢弃,如图24-4所示。

图24-4 在事务中执行额外工作

在使用自动事务功能时,这种额外的检查是不可能的,因为一旦调用SaveChanges方法,事务就会被提交。通过直接处理事务,我可以在决定结果之前执行其他任务。

更改事务隔离级别

事务的隔离级别指定数据库服务器如何处理尚未提交的更新。在某些应用程序中,重要的是将一个客户端所做的更改与其他客户端隔离开来,直到提交为止。在其他应用程序中,这种隔离不太重要,可以通过减少客户端免受他人未提交的更改的程度来提高性能。事务的隔离级别是通过从IsolationLevel枚举传递一个值来指定的,如表24-9所述。

表 24-9:IsolationLevel 值

名称 描述
Chaos 这个值定义得很差,但是当它被支持时,它的行为通常就像事务被禁用一样。对数据库所做的更改在提交之前对其他客户端是可见的,并且通常不支持回滚。SQL Server 不支持此隔离级别。
ReadUncomitted 此值表示通常支持的最低隔离级别。使用此隔离级别的事务可以读取其他尚未提交的事务所做的更改。
ReadComitted 如果没有指定值,则此值是默认的隔离级别。其他事务仍然可以在当前事务所做的更新之间插入或删除数据,这可能导致不一致的查询结果。
RepeatableRead 此值表示更高级别的隔离,防止其他事务修改当前事务读取的数据,这确保了一致的查询结果。
Serializable 此值通过防止其他事务将数据添加到数据库中已被当前事务读取的区域来提高RepeatableRead隔离级别。
Snapshot 此值表示最高级别的隔离,并确保事务处理每个事务都使用自己的数据快照。此隔离级别要求对无法使用 Entity Framework Core 执行的数据库进行更改。详见此处:https://docs.microsoft.com/en-us/dotnet/framework/data/adonet/sql/snapshot-isolation-in-sql-server

隔离级别越高,事务之间的交互就越少,但是数据库服务器必须使用更多的锁定,如果有多个客户机试图同时修改相同的数据,则会对性能产生影响。在选择隔离级别时,需要在性能和不一致的数据之间进行权衡。对于许多应用程序,特别是在 ASP.NET Core MVC 项目中,不需要更改隔离级别,因为默认值将提供可接受的折衷。然而,对于某些应用程序 —— 尤其是那些数据库特别复杂的应用程序 —— 事务之间的交互风险可能是一个问题。


避免高隔离级别陷阱

为了安全起见,您可能会倾向于选择高级别的事务隔离。这当然可以避免本章中描述的三个问题,但也可能会减慢您的应用程序。数据库服务器通过锁定对数据库部分的访问来增加事务隔离;事务隔离级别越高,所需的锁定就越多,更新开始排队的可能性就越大,这使得客户端需要等待较早的事务完成。选择错误的隔离级别,将无助于应用程序,且会降低性能。


理解脏读、幻影行和不可重复读取

当事务相互干扰时,可能会出现三个潜在的问题。当您决定事务的隔离级别时,您实际上是在告诉数据库您愿意接受哪些潜在问题,以及您愿意牺牲多少性能来避免它们。为了演示这三个问题,我对 Multi 控制器做了一些更改,如清单24-10所示。

清单 24-10:Controllers 文件夹下的 MultiController.cs 文件,使用隔离级别

using AdvancedApp.Models;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
using System;
using System.Linq;
using System.Data;
using Microsoft.EntityFrameworkCore;

namespace AdvancedApp.Controllers
{
    public class MultiController : Controller
    {
        private AdvancedContext context;
        private ILogger<MultiController> logger;
        private IsolationLevel level = IsolationLevel.ReadUncommitted;

        public MultiController(AdvancedContext ctx, ILogger<MultiController> log)
        {
            context = ctx;
            logger = log;
        }

        public IActionResult Index()
        {
            context.Database.BeginTransaction(level);
            return View("EditAll", context.Employees);
        }

        [HttpPost]
        public IActionResult UpdateAll(Employee[] employees)
        {
            context.Database.BeginTransaction(level);
            context.UpdateRange(employees);
            Employee temp = new Employee
            {
                SSN = "00-00-0000",
                FirstName = "Temporary",
                FamilyName = "Row",
                Salary = 0
            };
            context.Add(temp);
            context.SaveChanges();
            System.Threading.Thread.Sleep(5000);
            context.Remove(temp);
            context.SaveChanges();
            if (context.Employees.Sum(e => e.Salary) < 1_000_000)
            {
                context.Database.CommitTransaction();
            }
            else
            {
                context.Database.RollbackTransaction();
                logger.LogError("Salary total exceeds limit");
            }
            return RedirectToAction(nameof(Index));
        }

        public string ReadTest()
        {
            decimal firstSum = context.Employees.Sum(e => e.Salary);
            System.Threading.Thread.Sleep(5000);
            decimal secondSum = context.Employees.Sum(e => e.Salary);
            return $"Repeatable read results - first: {firstSum}, "
                + $"second: {secondSum}";
        }
    }
}

隔离级别被设置为BeginTransaction方法的一个参数,后者是Microsoft.EntityFrameworkCore命名空间中定义的扩展方法。在清单中,我选择了ReadUncomitted级别,它用于在Index aciton 中查询数据库并在UpdateAll action 中更新数据库的事务中。在UpdateAll方法中,我执行从用户收到的更新,向数据库添加一个临时Employee,然后再次删除它。只有当Salary值之和小于100万时才提交事务;否则将回滚。为了减缓UpdateAll方法的速度,并使查看事务交互的效果变得更容易,我添加了一个对Thread.Sleep方法的调用,该调用将事务提交或回滚延迟了5秒。

若要查看两个潜在问题,请使用dotnet run启动应用程序,打开两个浏览器窗口,并全部导航到 http://localhost:5000/multi。在第一个浏览器窗口中将 Alice Jones 的Salary值更改为900000,然后单击S【Save All】。在5秒休眠期过去之前重新加载另一个浏览器,您将看到图24-5所示的结果。

警告:你不会总是能够重新制造这些问题,特别是在实际的项目中,那里的条件更难控制。您应该根据原则上需要的隔离级别来选择您的隔离级别,即使您不能在测试期间重新创建一个特定的问题。

图24-5 另一项事务所作的更改

即使这些更改随后回滚,在一定时间内,ReadUncomitted隔离级别也允许在另一个事务中读取更改。Temporary 行是幻影行示例,Alice 显示的Salary值是脏读示例。

要查看第三个问题,请导航到其中一个窗口中的 http://localhost:5000/multi/readtest。该请求的目标是清单24-10中添加的ReadTest action,它在暂停5秒之前和之后对数据库执行相同的查询。此方法返回一个字符串,这样我就不必向项目中添加视图,并且它将产生一个显示两个读取的相同值的结果,如下所示:

Repeatable read results - first: 500000.00, second: 500000.00

使用另一个浏览器窗口导航到 http://localhost:5000/multi,将 Alice 的薪资更改为 90000,然后单击【Save All】按钮。在5秒结束之前,重新加载第一个浏览器窗口来重复这两个查询。这一次,您将看到每个查询都收到不同的结果。

Repeatable read results - first: 1200000.00, second: 500000.00

这是一个不可重复的读取问题,同一事务执行的两个查询返回不同的结果。如果您更改控制器中事务使用的隔离级别,您将发现看不到这些问题。

总结

在本章中,我描述了 Entity Framework Core 支持事务的方式,从自动事务功能开始,然后直接处理事务。我解释了如何禁用事务,如何作为事务的一部分执行额外的工作,以及如何告诉 Entity Framework Core 何时提交或回滚对数据库的更改。在结束这一章时,我解释了可以用于事务的不同隔离级别,以及每个事务在性能方面所代表的权衡。

这就是我要告诉您的关于 ASP.NET Core MVC 开发的 Entity Framework Core 的全部内容。我首先介绍了使用数据库的基础知识,描述了您可能遇到的常见问题,并描述了所有关键的 Entity Framework Core 功能。我希望您和我一样喜欢阅读这本书,并祝你在 ASP.NET Core MVC和 Entity Framework Core 项目中一切顺利。

;

© 2018 - IOT小分队文章发布系统 v0.3